﻿using BUGCOME.Infrastructure.Attribute;
using BUGCOME.Infrastructure.Helper;
using BUGCOME.Model.Auth;
using BUGCOME.Model.Enum.File;
using BUGCOME.Model.File;
using BUGCOME.Model.File.Dto;
using BUGCOME.Model.File.Outher;
using BUGCOME.Service.File.IServices;
using Dm.util;
using Mapster;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using System.Linq.Expressions;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace BUGCOME.Service.File
{
    /// <summary>
    /// 文件服务实现类
    /// 处理文件CRUD、上传下载、分片上传及权限统计（适配storageType=1本地存储路径规则）
    /// </summary>
    [AppService(ServiceType = typeof(IFileBaseService), ServiceLifetime = LifeTime.Transient)]
    public class FileBaseService : BaseService<FileBase>, IFileBaseService
    {
        private readonly Upload _uploadSettings;
        private readonly StorageSettings _storageSettings;
        private OptionsSetting optionsSetting;
        private readonly string _aliyunDomainUrl;
        private IWebHostEnvironment webHostEnvironment;
        // 路径格式校验正则：匹配 "字母+:/或:\" 开头（如 D:/、E:\）
        private readonly Regex _localPathRegex = new Regex(@"^[A-Za-z]:[/\\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);


        /// <summary>
        /// 构造函数
        /// 初始化配置与数据库连接（适配本地存储路径规则）
        /// </summary>
        public FileBaseService(IOptions<OptionsSetting> options, IWebHostEnvironment webHostEnvironment)
        {
            optionsSetting = options.Value;
            _uploadSettings = optionsSetting.Upload;
            _storageSettings = optionsSetting.StorageSettings;
            _aliyunDomainUrl = _storageSettings.AliOss.BucketName.Contains(".") ? _storageSettings.AliOss.BucketName
                : $"{_storageSettings.AliOss.BucketName}.{_storageSettings.AliOss.Endpoint}";

            // 初始化检查：确保PathMappings存在outher默认路径配置
            if (!_storageSettings.LocalStorage.PathMappings.ContainsKey("outher"))
            {
                _storageSettings.LocalStorage.PathMappings["outher"] = "D:/outher/";
            }

            this.webHostEnvironment = webHostEnvironment;
        }

        #region 基础CRUD实现
        /// <summary>
        /// 分页查询文件列表
        /// 根据条件筛选并返回分页数据
        /// </summary>
        public async Task<PagedInfo<FileVo>> SelectFileListAsync(FileQueryDto dto)
        {
            if (dto == null)
                throw new ArgumentNullException(nameof(dto), "查询参数不能为空");

            var exp = Expressionable.Create<FileBase>();
            // 修复：仅对可空类型使用HasValue
            exp.AndIF(dto.StorageType.HasValue, f => f.StorageType == dto.StorageType.Value);
            exp.AndIF(dto.Visibility.HasValue, f => f.Visibility == dto.Visibility.Value);
            exp.AndIF(dto.FileId.HasValue, f => f.Id == dto.FileId.Value);
            exp.AndIF(!string.IsNullOrEmpty(dto.RealName), f => f.RealName.Contains(dto.RealName));
            exp.AndIF(dto.CreateTimeStart.HasValue, f => f.CreateTime >= dto.CreateTimeStart.Value);
            exp.AndIF(dto.CreateTimeEnd.HasValue, f => f.CreateTime <= dto.CreateTimeEnd.Value);
            var where = exp.ToExpression();

            var pagedData = await GetPagesAsync<FileBase, FileVo>(
                queryable => queryable
                    .LeftJoin<FileFree>((f, ff) => f.Id == ff.FileId)
                    .LeftJoin<FilePayPerDownload>((f, ff, fppd) => f.Id == fppd.FileId)
                    .LeftJoin<FileMember>((f, ff, fppd, fm) => f.Id == fm.FileId)
                    .Select((f, ff, fppd, fm) => new FileVo
                    {
                        Id = f.Id,
                        RealName = f.RealName,
                        Name = f.Name,
                        FileUrl = f.FileUrl,
                        StorageType = f.StorageType,
                        StoragePath = f.StoragePath,
                        AccessUrl = f.AccessUrl,
                        FileHash = f.FileHash,
                        FileSize = f.FileSize,
                        FileType = f.FileType,
                        FileExt = f.FileExt,
                        Visibility = f.Visibility,
                        Status = f.Status,
                        CreateBy = f.CreateBy,
                        CreateTime = f.CreateTime,
                        UpdateBy = f.UpdateBy,
                        UpdateTime = f.UpdateTime,
                        FileFree = ff,
                        FilePayPerDownload = fppd,
                        FileMember = fm
                    }),
                where,
                dto,
                f => f.CreateTime,
                OrderByType.Desc
            );

            foreach (var fileVo in pagedData.Result)
            {
                fileVo.FileFree ??= new FileFree();
                fileVo.FilePayPerDownload ??= new FilePayPerDownload();
                fileVo.FileMember ??= new FileMember();
            }

            return pagedData;
        }

        /// <summary>
        /// 根据ID查询文件详情
        /// 关联查询文件扩展配置信息
        /// </summary>
        public async Task<FileVo> SelectFileByIdAsync(long fileId)
        {
            if (fileId <= 0)
                throw new ArgumentOutOfRangeException(nameof(fileId), "文件ID必须大于0");

            // 第一步：先查询数据，不处理空值
            var queryResult = await Context.Queryable<FileBase>()
                .LeftJoin<FileFree>((f, ff) => f.Id == ff.FileId)
                .LeftJoin<FilePayPerDownload>((f, ff, fppd) => f.Id == fppd.FileId)
                .LeftJoin<FileMember>((f, ff, fppd, fm) => f.Id == fm.FileId)
                .Where(f => f.Id == fileId)
                .Select((f, ff, fppd, fm) => new
                {
                    FileBase = f,
                    FileFree = ff,
                    FilePayPerDownload = fppd,
                    FileMember = fm
                })
                .FirstAsync();

            // 如果查询结果为空，返回一个空的FileVo
            if (queryResult == null)
                return new FileVo();

            // 第二步：在内存中构建FileVo，并处理空值
            var fileVo = new FileVo
            {
                Id = queryResult.FileBase.Id,
                RealName = queryResult.FileBase.RealName,
                Name = queryResult.FileBase.Name,
                FileUrl = queryResult.FileBase.FileUrl,
                StorageType = queryResult.FileBase.StorageType,
                StoragePath = queryResult.FileBase.StoragePath,
                AccessUrl = queryResult.FileBase.AccessUrl,
                FileHash = queryResult.FileBase.FileHash,
                FileSize = queryResult.FileBase.FileSize,
                FileType = queryResult.FileBase.FileType,
                FileExt = queryResult.FileBase.FileExt,
                Visibility = queryResult.FileBase.Visibility,
                Status = queryResult.FileBase.Status,
                CreateBy = queryResult.FileBase.CreateBy,
                CreateTime = queryResult.FileBase.CreateTime,
                UpdateBy = queryResult.FileBase.UpdateBy,
                UpdateTime = queryResult.FileBase.UpdateTime,

                // 在内存中处理空值，避免SQL查询中出现不支持的操作
                FileFree = queryResult.FileFree ?? new FileFree(),
                FilePayPerDownload = queryResult.FilePayPerDownload ?? new FilePayPerDownload(),
                FileMember = queryResult.FileMember ?? new FileMember()
            };

            return fileVo;
        }

      

        /// <summary>
        /// 更新文件信息
        /// 支持文件存储位置迁移与配置更新（适配storageType=1路径规则）
        /// </summary>
        public async Task<int> UpdateFileAsync(FileDto dto, long currentUserId,string currentUserName)
        {
            if (dto == null || !dto.Id.HasValue)
                throw new ArgumentNullException(nameof(dto.Id), "文件ID不能为空");

            var oldFile = await Context.Queryable<FileBase>().FirstAsync(f => f.Id == dto.Id.Value)
                ?? throw new KeyNotFoundException($"文件ID{dto.Id.Value}不存在");

            // 本地存储路径规则适配：如果修改visibility或storagePath，需要重新计算路径
            bool needUpdatePath = oldFile.StorageType == (int)StoreType.LOCAL
                && (dto.Visibility.HasValue && dto.Visibility.Value != oldFile.Visibility
                    || dto.StoragePath.HasValue && dto.StoragePath.Value != oldFile.StoragePath);

            // 修复：检查可空类型是否有值
            bool needMove = dto.StorageType == 1 && dto.StoragePath.HasValue
                && (dto.StorageType != oldFile.StorageType || dto.StoragePath.Value != oldFile.StoragePath);
            if (needMove && !await MovePhysicalFileAsync(oldFile, dto.StorageType, dto.StoragePath.Value))
                throw new InvalidOperationException("文件迁移失败");

            var fileBase = dto.Adapt<FileBase>();
            fileBase.UpdateId = currentUserId;
            fileBase.UpdateBy = currentUserName;
            fileBase.UpdateTime = DateTime.Now;
            fileBase.CreateId = oldFile.CreateId;
            fileBase.CreateBy = oldFile.CreateBy;
            fileBase.CreateTime = oldFile.CreateTime;
            fileBase.Id = oldFile.Id; // 保持主键不变

            // 本地存储路径更新
            if (oldFile.StorageType == (int)StoreType.LOCAL && needUpdatePath)
            {
                int newVisibility = dto.Visibility.HasValue ? dto.Visibility.Value : oldFile.Visibility;
                int newStoragePath = dto.StoragePath.HasValue ? dto.StoragePath.Value : oldFile.StoragePath;
                string newRootPath = GetLocalStoragePathWithVisibility(newStoragePath, newVisibility);
                string newFilePath = Path.Combine(newRootPath, oldFile.Name);
                fileBase.FileUrl = newFilePath.Replace("\\", "/");
                fileBase.AccessUrl = GenerateAccessUrl((int)StoreType.LOCAL, newFilePath, oldFile.Name, newStoragePath, newVisibility);
                fileBase.Visibility = newVisibility;
                fileBase.StoragePath = newStoragePath;
            }
            // 修复：检查可空类型是否有值
            else if (needMove && dto.StorageType == (int)StoreType.LOCAL)
            {
                string newPath = GetActualStoragePath(dto.StorageType, dto.StoragePath.Value, dto.Visibility ?? oldFile.Visibility);
                fileBase.FileUrl = Path.Combine(newPath, fileBase.Name).Replace("\\", "/");
                fileBase.AccessUrl = GenerateAccessUrl(dto.StorageType, newPath, fileBase.Name, dto.StoragePath.Value, dto.Visibility ?? oldFile.Visibility);
            }

            var result = await UseTranAysnc(async () =>
            {
                int rows = await Context.Updateable<FileBase>(fileBase)
                    .IgnoreNullColumns(true)
                    .IgnoreColumns(f => new { f.CreateBy, f.CreateTime, f.Id, f.FileHash, f.FileSize, f.FileExt }) // 忽略不可修改字段
                    .Where(f => f.Id == dto.Id.Value)
                    .ExecuteCommandAsync();

                if (rows > 0 && dto.Visibility.HasValue && dto.Visibility.Value != oldFile.Visibility)
                {
                    // 可见性变更，清理旧配置，保存新配置
                    await Context.Deleteable<FileFree>().Where(ff => ff.FileId == dto.Id.Value).ExecuteCommandAsync();
                    await Context.Deleteable<FilePayPerDownload>().Where(fppd => fppd.FileId == dto.Id.Value).ExecuteCommandAsync();
                    await Context.Deleteable<FileMember>().Where(fm => fm.FileId == dto.Id.Value).ExecuteCommandAsync();
                    await SaveFileConfigAsync(dto.Id.Value, dto);
                }
            });

            return result.IsSuccess ? 1 : 0;
        }

        /// <summary>
        /// 批量删除文件
        /// 物理删除文件并清理关联配置
        /// </summary>
        public async Task<int> DeleteFileByIdsAsync(long[] fileIds)
        {
            if (fileIds == null || fileIds.Length == 0)
                throw new ArgumentNullException(nameof(fileIds), "文件ID数组不能为空");

            var fileInfos = await Context.Queryable<FileBase>()
                .Where(f => fileIds.Contains(f.Id))
                .Select(f => new { f.Id, f.StorageType, f.FileUrl, f.Name, f.StoragePath, f.Visibility })
                .ToListAsync();

            if (!fileInfos.Any())
                return 0;

            var tranResult = await UseTranAysnc(async () =>
            {
                foreach (var file in fileInfos)
                {
                    if (!await DeletePhysicalFileAsync(file))
                        throw new Exception($"文件ID：{file.Id} 物理删除失败");
                }

                await Context.Deleteable<FileFree>().In(ff => ff.FileId, fileIds).ExecuteCommandAsync();
                await Context.Deleteable<FilePayPerDownload>().In(fppd => fppd.FileId, fileIds).ExecuteCommandAsync();
                await Context.Deleteable<FileMember>().In(fm => fm.FileId, fileIds).ExecuteCommandAsync();
                int deleteCount = await Context.Deleteable<FileBase>().In(f => f.Id, fileIds).ExecuteCommandAsync();
            });

            return tranResult.IsSuccess ? fileInfos.Count : 0;
        }


        /// <summary>
        /// 将上传文件保存到本地存储的核心异步方法
        /// 包含文件校验、路径处理、图片压缩、URL生成及数据库记录等完整流程
        /// </summary>
        /// <param name="rootPath">系统根路径</param>
        /// <param name="dto">上传请求数据传输对象，包含文件、存储配置等信息</param>
        /// <param name="finalFileName">文件最终保存的主名称（不含扩展名）</param>
        /// <param name="userId">当前操作用户ID</param>
        /// <param name="userName">当前操作用户名称</param>
        /// <returns>保存成功返回文件信息实体，失败返回null</returns>
        /// <exception cref="ArgumentException">文件名或目录名包含无效字符时抛出</exception>
        public async Task<FileBase> SaveFileToLocalAsync(string rootPath, UploadDto dto, string finalFileName, long userId, string userName)
        {
            // 校验文件名和目录的有效性，防止非法路径注入
            if (finalFileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
            {
                throw new ArgumentException("文件名包含无效字符");
            }

            var fileDir = dto.FileDir;
            if (fileDir != null && fileDir.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
            {
                throw new ArgumentException("目录名包含无效字符");
            }

            // 获取文件扩展名并拼接最终文件名（主名称+扩展名）
            string fileExt = Path.GetExtension(dto.File.FileName).ToLower();
            string fileName = finalFileName + fileExt;

            // 根据存储路径配置和可见性获取实际存储根路径
            string storageRootPath = GetLocalStoragePathWithVisibility(dto.StoragePath ?? 0, dto.Visibility);
            string finalRootPath = string.IsNullOrWhiteSpace(storageRootPath) ? rootPath : storageRootPath;

            // 构建完整文件路径：根路径 + 自定义目录 + 文件名
            string filePath = GetDirPath(fileDir);
            string finalFilePath = Path.Combine(finalRootPath, filePath.TrimStart('/', '\\'), fileName);
            // 计算文件大小（KB）并保留两位小数
            double fileSize = Math.Round(dto.File.Length / 1024.0, 2);

            // 确保存储目录存在，不存在则创建
            if (!Directory.Exists(Path.GetDirectoryName(finalFilePath)))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(finalFilePath));
            }

            // 图片压缩处理逻辑：仅当满足压缩条件时执行压缩保存
            var imageExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" };
            bool isImage = imageExtensions.Contains(fileExt);
            if (dto.Quality > 0 && dto.Quality <= 100 && isImage)
            {
                await SaveCompressedImageAsync(dto.File, finalFilePath, dto.Quality);
            }
            else
            {
                // 非图片或无需压缩时，直接复制文件流保存
                using (var stream = new FileStream(finalFilePath, FileMode.Create))
                {
                    await dto.File.CopyToAsync(stream);
                }
            }

            // 生成文件访问URL（根据存储类型、可见性等配置动态生成）
            string accessUrl = GenerateAccessUrl(
                (int)StoreType.LOCAL,
                finalRootPath,
                fileName,
                dto.StoragePath ?? 0,
                dto.Visibility,
                filePath);

            // 构建文件信息实体，封装文件元数据
            FileBase file = new FileBase(dto.File.FileName, fileName, fileExt, (long)fileSize, filePath, userId, userName)
            {
                StorageType = (int)StoreType.LOCAL, // 标记存储类型为本地
                FileHash = RandomHelper.HashFileName(),
                FileType = dto.File.ContentType,    // 文件MIME类型
                FileUrl = finalFilePath.Replace("\\", "/"), // 本地物理路径（统一路径分隔符）
                AccessUrl = accessUrl,              // 访问URL
                Status = dto.Status ?? 1            // 文件状态（默认正常）
            };

            // 数据库事务保存：确保文件记录和URL更新原子性
            var result = await UseTranAysnc(async () =>
            {
                file.Id = await InsertReturnBigIdentityAsync(file); // 插入文件记录并获取ID    
            });

            // 事务失败回滚：删除已保存的物理文件，保证数据一致性
            if (!result.IsSuccess)
            {
                if (LocalFileHelper.Exists(file.FileUrl))
                {
                    LocalFileHelper.Delete(file.FileUrl);
                }
                return null;
            }

            return file; // 返回完整的文件信息实体
        }
        #endregion

        #region 上传下载实现
        /// <summary>
        /// 校验文件哈希是否已存在
        /// 用于文件去重判断
        /// </summary>
        public async Task<bool> CheckFileHashExistsAsync(string fileHash)
        {
            return string.IsNullOrEmpty(fileHash)
                ? false
                : await Context.Queryable<FileBase>().AnyAsync(f => f.FileHash == fileHash);
        }

     

        /// <summary>
        /// 文件下载
        /// 含权限校验、下载统计与日志记录
        /// </summary>
        public async Task<FileDownloadResult> DownloadFileAsync(long fileId, long userId)
        {
            var fileVo = await SelectFileByIdAsync(fileId)
                ?? throw new KeyNotFoundException($"文件ID[{fileId}]不存在");

            if (fileVo.Status != (int)FileStatus.Normal)
                throw new InvalidOperationException($"文件状态异常（状态码：{fileVo.Status}）");

            var accessResult = await CheckFileAccessAsync(fileId, userId);
            if (!accessResult.HasAccess)
                throw new UnauthorizedAccessException(accessResult.Message);

            await UpdateDownloadStatsAsync(fileId, fileVo.Visibility, userId);
            var fileStream = await GetFileStreamAsync(fileVo);
            if (fileStream == null)
                throw new IOException("获取文件流失败");

            await RecordDownloadLogAsync(fileId, userId);

            return new FileDownloadResult
            {
                Stream = fileStream,
                ContentType = fileVo.FileType,
                FileName = fileVo.RealName
            };
        }

        /// <summary>
        /// 文件分片下载
        /// 解析范围请求并返回对应文件流
        /// </summary>
        public async Task<FileRangeDownloadResult> RangeDownloadFileAsync(long fileId, long userId, string rangeHeader)
        {
            var fileVo = await SelectFileByIdAsync(fileId)
                ?? throw new KeyNotFoundException($"文件ID[{fileId}]不存在");

            var accessResult = await CheckFileAccessAsync(fileId, userId);
            if (!accessResult.HasAccess)
                throw new UnauthorizedAccessException(accessResult.Message);

            var (start, end) = ParseRangeHeader(rangeHeader, fileVo.FileSize);
            var contentLength = end - start + 1;
            var contentRange = $"bytes {start}-{end}/{fileVo.FileSize}";
            var fileStream = await GetRangeFileStreamAsync(fileVo, start, end);

            return new FileRangeDownloadResult
            {
                Stream = fileStream,
                ContentType = fileVo.FileType,
                FileName = fileVo.RealName,
                ContentRange = contentRange,
                ContentLength = contentLength
            };
        }
        #endregion

        #region 分片上传实现
        /// <summary>
        /// 初始化分片上传
        /// 生成上传ID与分片信息（适配storageType=1路径规则）
        /// </summary>
        public async Task<MultipartUploadInitResult> InitMultipartUploadAsync(MultipartUploadInitDto dto, long userId)
        {
            if (dto == null)
                throw new ArgumentNullException(nameof(dto), "分片上传参数不能为空");

            string uploadId = Guid.NewGuid().ToString("N");
            string fileHash = ComputeTempFileHash(dto.FileName, dto.FileSize);

            if (await CheckFileHashExistsAsync(fileHash))
                throw new InvalidOperationException("文件已存在，无需重复上传");

            await Context.Insertable<MultipartUploadMeta>(new MultipartUploadMeta
            {
                UploadId = uploadId,
                FileName = dto.FileName,
                FileSize = dto.FileSize,
                FileHash = fileHash,
                FileExt = Path.GetExtension(dto.FileName).ToLower(),
                // 修复：存储类型应为可空int
                StorageType =  (int)dto.StorageType,
                StoragePath = dto.StoragePath,
                Visibility = dto.Visibility,
                CreateBy = userId,
                CreateTime = DateTime.Now,
                ExpireTime = DateTime.Now.AddHours(24) // 24小时过期
            }).ExecuteCommandAsync();

            long chunkSize = dto.ChunkSize ?? 5 * 1024 * 1024;
            int chunkCount = (int)Math.Ceiling((double)dto.FileSize / chunkSize);

            return new MultipartUploadInitResult
            {
                UploadId = uploadId,
                ChunkCount = chunkCount,
                ChunkSize = chunkSize,
                FileHash = fileHash
            };
        }

        /// <summary>
        /// 上传分片文件
        /// 保存分片并记录分片信息（适配storageType=1路径规则）
        /// </summary>
        public async Task UploadPartAsync(MultipartUploadPartDto dto)
        {
            if (dto == null || string.IsNullOrEmpty(dto.UploadId) || dto.File == null || dto.File.Length == 0)
                throw new ArgumentException("分片上传参数不完整");

            var uploadMeta = await Context.Queryable<MultipartUploadMeta>()
                .Where(m => m.UploadId == dto.UploadId)
                .FirstAsync() ?? throw new KeyNotFoundException($"上传ID[{dto.UploadId}]不存在");

            if (dto.ChunkIndex < 0 || dto.ChunkIndex >= uploadMeta.ChunkCount)
                throw new ArgumentOutOfRangeException(nameof(dto.ChunkIndex), "分片序号无效");

            // 本地存储分片路径：按visibility规则生成
            string chunkPath = GetChunkStoragePath(uploadMeta.UploadId, dto.ChunkIndex, uploadMeta.StoragePath, (int)uploadMeta.Visibility);
            await SaveChunkToStorageAsync(dto.File, chunkPath, (StoreType)uploadMeta.StorageType);

            await Context.Insertable<MultipartUploadChunk>(new MultipartUploadChunk
            {
                UploadId = dto.UploadId,
                ChunkIndex = dto.ChunkIndex,
                ChunkPath = chunkPath.Replace("\\", "/"),
                ChunkSize = dto.File.Length,
                CreateTime = DateTime.Now
            }).ExecuteCommandAsync();
        }

        /// <summary>
        /// 完成分片上传
        /// 合并分片并保存文件信息（适配storageType=1路径规则）
        /// </summary>
        public async Task<FileUploadResult> CompleteMultipartUploadAsync(MultipartUploadCompleteDto dto, long userId)
        {
            if (dto == null || string.IsNullOrEmpty(dto.UploadId))
                throw new ArgumentException("分片上传完成参数不完整");

            var uploadMeta = await Context.Queryable<MultipartUploadMeta>()
                .Where(m => m.UploadId == dto.UploadId)
                .FirstAsync() ?? throw new KeyNotFoundException($"上传ID[{dto.UploadId}]不存在");

            var uploadedChunks = await Context.Queryable<MultipartUploadChunk>()
                .Where(c => c.UploadId == dto.UploadId)
                .OrderBy(c => c.ChunkIndex)
                .ToListAsync();

            if (uploadedChunks.Count != uploadMeta.ChunkCount)
                throw new InvalidOperationException($"存在未上传的分片，已上传{uploadedChunks.Count}个，需上传{uploadMeta.ChunkCount}个");

            string targetFilePath = string.Empty;
            List<string> tempChunkPaths = uploadedChunks.Select(c => c.ChunkPath).ToList();
            FileBase newFileBase = null;
            StoreType storeType = (StoreType)uploadMeta.StorageType;
            int visibility = dto.Visibility.HasValue ? dto.Visibility.Value : uploadMeta.Visibility;

            try
            {
                // 本地存储：按visibility规则生成目标路径
                targetFilePath = storeType == StoreType.LOCAL
                    ? Path.Combine(
                        GetLocalStoragePathWithVisibility(uploadMeta.StoragePath, visibility),
                        "complete",
                        $"{uploadMeta.FileHash}{uploadMeta.FileExt}"
                    )
                    : Path.Combine("complete", $"{uploadMeta.FileHash}{uploadMeta.FileExt}");

                newFileBase = await MergeChunksAndSaveAsync(uploadMeta, uploadMeta.FileName, targetFilePath, userId, visibility, uploadMeta.StoragePath);
                if (newFileBase == null || string.IsNullOrEmpty(newFileBase.FileUrl))
                    throw new Exception("分片合并失败，未生成完整文件");
            }
            catch (Exception ex)
            {
                // 清理临时分片
                foreach (var chunkPath in tempChunkPaths)
                {
                    await DeletePhysicalFileAsync(new { StorageType = (int)storeType, FileUrl = chunkPath, Visibility = visibility });
                }
                throw new Exception($"分片合并失败：{ex.Message}（已清理临时分片）", ex);
            }

            try
            {
                var tranResult = await UseTranAysnc(async () =>
                {
                    await CleanupMultipartTempDataAsync(dto.UploadId);
                });

                if (!tranResult.IsSuccess)
                {
                    // 清理完整文件和临时分片
                    await DeletePhysicalFileAsync(new { StorageType = (int)storeType, FileUrl = targetFilePath, Visibility = visibility });
                    foreach (var chunkPath in tempChunkPaths)
                    {
                        await DeletePhysicalFileAsync(new { StorageType = (int)storeType, FileUrl = chunkPath, Visibility = visibility });
                    }
                    throw new Exception("事务执行失败，已清理完整文件和临时分片");
                }
            }
            catch (Exception ex)
            {
                if (!string.IsNullOrEmpty(targetFilePath))
                    await DeletePhysicalFileAsync(new { StorageType = (int)storeType, FileUrl = targetFilePath, Visibility = visibility });
                foreach (var chunkPath in tempChunkPaths)
                    await DeletePhysicalFileAsync(new { StorageType = (int)storeType, FileUrl = chunkPath, Visibility = visibility });

                throw new Exception($"分片上传完成失败：{ex.Message}（已清理残留文件）", ex);
            }

            return new FileUploadResult
            {
                FileId = newFileBase.Id,
                FileName = newFileBase.RealName,
                AccessUrl = newFileBase.AccessUrl,
                StorageType = newFileBase.StorageType
            };
        }
        #endregion

        #region 统计与权限实现
        /// <summary>
        /// 获取文件下载统计数据
        /// 返回总次数与每日统计
        /// </summary>
        public async Task<FileDownloadStatsVo> GetFileDownloadStatsAsync(FileStatsQueryDto dto)
        {
            var query = Context.Queryable<FileDownloadRecord>()
                .Where(r => r.DownloadTime != null)
                .WhereIF(dto.FileId.HasValue, r => r.FileId == dto.FileId.Value)
                .WhereIF(dto.UserId.HasValue, r => r.UserId == dto.UserId.Value)
                .WhereIF(dto.StartTime.HasValue, r => r.DownloadTime >= dto.StartTime.Value)
                .WhereIF(dto.EndTime.HasValue, r => r.DownloadTime <= dto.EndTime.Value);

            int totalCount = await query.CountAsync();

            // 按日期分组统计
            var dailyStats = await query
                .GroupBy(r => r.DownloadTime)
                .Select(g => new DailyDownloadStats
                {
                    Date = g.DownloadTime,
                    Count = SqlFunc.AggregateCount(g.Id),
                })
                .OrderBy(g => g.Date)
                .ToListAsync();

            return new FileDownloadStatsVo
            {
                TotalCount = totalCount,
                DailyStats = dailyStats
            };
        }

        /// <summary>
        /// 校验文件访问权限
        /// 根据用户类型与文件可见性判断
        /// </summary>
        public async Task<FileAccessResult> CheckFileAccessAsync(long fileId, long userId)
        {
            var fileVo = await SelectFileByIdAsync(fileId);
            if (fileVo == null)
                return new FileAccessResult(false, "文件不存在");

            if (fileVo.Status == (int)FileStatus.Disabled)
                return new FileAccessResult(false, "文件已被禁用");
            if (fileVo.Status == (int)FileStatus.ScheduledDelete)
                return new FileAccessResult(false, "文件已过期待删除");

            string userType = await GetUserTypeAsync(userId);
            if (userType == ((int)UserType.Personal).ToString())
            {
                return new FileAccessResult(true, "个人用户免费下载");
            }

            return fileVo.Visibility switch
            {
                (int)FileVisibility.Free => await CheckFreeFileAccessAsync(fileVo, userId),
                (int)FileVisibility.PayPerDownload => await CheckPayFileAccessAsync(fileId, userId),
                (int)FileVisibility.MemberOnly => await CheckMemberFileAccessAsync(fileVo, userId),
                _ => new FileAccessResult(false, "不支持的文件可见性类型")
            };
        }
        #endregion

        #region 核心私有辅助方法（适配storageType=1路径规则）
        /// <summary>
        /// 校验路径是否以“字母+:/”开头（本地存储专用）
        /// </summary>
        private bool IsValidLocalPathFormat(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
                return false;
            return _localPathRegex.IsMatch(path.Trim());
        }

        /// <summary>
        /// 获取本地存储路径（含visibility规则校验，仅storageType=1时调用）
        /// </summary>
        private string GetLocalStoragePathWithVisibility(int storagePath, int visibility)
        {
            // 1. 从PathMappings获取基础路径
            string basePath = _storageSettings.LocalStorage.PathMappings.TryGetValue(
                storagePath.ToString(), out var path) ? path : string.Empty;

            // 2. 处理特殊键“1”=当前目录
            if (storagePath == 1)
            {
                basePath = Directory.GetCurrentDirectory();
            }

            // 3. 根据visibility规则修正路径
            string finalPath = visibility switch
            {
                1 => HandleVisibility1Path(basePath),       // 公开免费
                2 or 3 => HandleVisibility2Or3Path(basePath), // 按次收费/会员专属
                _ => throw new ArgumentOutOfRangeException(nameof(visibility), "不支持的可见性类型（仅支持1/2/3）")
            };

            // 4. 规范化路径（统一斜杠，确保目录结尾有分隔符）
            string normalizedPath = Path.GetFullPath(finalPath)
                .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
                + Path.DirectorySeparatorChar;

            // 5. 确保目录存在
            if (!Directory.Exists(normalizedPath))
            {
                Directory.CreateDirectory(normalizedPath);
            }

            return normalizedPath;
        }

        /// <summary>
        /// 处理visibility=1的路径（公开免费）
        /// 空路径时采用 /uploads（项目根目录下的uploads文件夹）
        /// </summary>
        private string HandleVisibility1Path(string basePath)
        {
            // 空路径（basePath为空）：使用项目根目录下的uploads文件夹
            if (string.IsNullOrWhiteSpace(basePath))
            {
                return Path.Combine(Path.Combine(webHostEnvironment.WebRootPath));
            }

            // 基础路径为绝对路径：直接使用
            if (IsValidLocalPathFormat(basePath))
                return basePath;

            // 基础路径为相对路径：直接使用（基于项目根目录）
            return Path.Combine(Directory.GetCurrentDirectory(), basePath.TrimStart('/', '\\'));
        }

        /// <summary>
        /// 处理visibility=2或3的路径（按次收费/会员专属）
        /// </summary>
        private string HandleVisibility2Or3Path(string basePath)
        {
            if (IsValidLocalPathFormat(basePath))
                return basePath;

            // 强制默认D:/outher/（优先级：配置outher > 硬编码）
            return _storageSettings.LocalStorage.PathMappings["outher"] ?? "D:/outher/";
        }

        /// <summary>
        /// 获取实际存储路径（适配storageType=1+visibility规则）
        /// </summary>
        private string GetActualStoragePath(int storageType, int storagePath, int visibility)
        {
            return storageType switch
            {
                (int)StoreType.LOCAL => GetLocalStoragePathWithVisibility(storagePath, visibility),
                (int)StoreType.ALIYUN => _storageSettings.AliOss.BucketName,
                _ => throw new NotSupportedException($"不支持的存储类型：{storageType}")
            };
        }

        /// <summary>
        /// 计算临时文件哈希（用于分片上传初始化）
        /// </summary>
        private string ComputeTempFileHash(string fileName, long fileSize)
        {
            using var md5 = MD5.Create();
            byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes($"{fileName}{fileSize}{Guid.NewGuid()}"));
            return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        }

        /// <summary>
        /// 文件校验
        /// 验证扩展名与文件大小是否合法
        /// </summary>
        private void ValidateFile(string fileExt, long fileSizeBytes)
        {
            if (_uploadSettings.NotAllowedExt?.Contains(fileExt) == true)
                throw new InvalidDataException($"禁止上传{fileExt}类型文件");

            long maxSizeBytes = _uploadSettings.MaxSize * 1024 * 1024;
            if (fileSizeBytes > maxSizeBytes)
                throw new InvalidDataException($"文件大小超过限制（最大{_uploadSettings.MaxSize}MB）");
        }


        /// <summary>
        /// 生成存储目录（按日期或自定义目录）
        /// </summary>
        private string GetDirPath(string customDir = "")
        {
            DateTime now = DateTime.Now;
            string timeDir = $"{now:yyyy}/{now:MM}/{now:dd}";
            return string.IsNullOrEmpty(customDir)
                ? timeDir
                : Path.Combine(customDir.Trim('/'), timeDir).Replace("\\", "/");
        }

      

        /// <summary>
        /// 保存到阿里云
        /// 上传文件到OSS并返回信息
        /// </summary>
        private async Task<FileBase> SaveToAliyun(UploadDto dto, IFormFile file, long userId, string fileName, string storeDir, string ext, long sizeBytes, string hash, int Quality)
        {
            string objectName = Path.Combine(storeDir, $"{fileName}{ext}").Replace("\\", "/");
            HttpStatusCode statusCode;

            if (Quality > 0 && IsImageExt(ext))
            {
                using var stream = new MemoryStream();
                await CompressImageAsync(file, stream, Quality, ext);
                stream.Position = 0;
                statusCode = AliyunOssHelper.PutObjectFromFile(stream, objectName, file.ContentType, _storageSettings.AliOss);
            }
            else
            {
                using var stream = file.OpenReadStream();
                statusCode = AliyunOssHelper.PutObjectFromFile(stream, objectName, file.ContentType, _storageSettings.AliOss);
            }

            if (statusCode != HttpStatusCode.OK)
                throw new Exception($"阿里云OSS上传失败，状态码：{statusCode}");

            string accessUrl = $"https://{_aliyunDomainUrl}/{objectName}";
            return await InsertFileBaseAsync(file, userId, fileName, ext, sizeBytes, hash,
                (int)StoreType.ALIYUN, dto.StoragePath ?? 0, objectName, accessUrl, dto.Status ?? 1, dto.Visibility);
        }

        /// <summary>
        /// 将文件保存到阿里云OSS存储的异步方法
        /// 处理文件上传、图片压缩并返回文件信息实体
        /// </summary>
        /// <param name="sysFile">系统文件信息对象（用于传递基础元数据）</param>
        /// <param name="uploadDto">上传请求数据传输对象，包含存储配置、压缩质量等参数</param>
        /// <param name="file">待上传的表单文件对象</param>
        /// <returns>上传成功后的文件信息实体</returns>
        /// <exception cref="Exception">阿里云OSS上传失败时抛出异常</exception>
        public async Task<FileBase> SaveFileToAliyunAsync(FileBase sysFile, UploadDto uploadDto, IFormFile file)
        {
            // 提取文件基础信息
            string fileName = sysFile.Name; // 使用系统生成的保存文件名（不含扩展名）
            string ext = sysFile.FileExt;           // 文件扩展名
            string storeDir = sysFile.FileUrl;     // 存储目录路径
            long sizeBytes = file.Length;           // 文件字节大小
            string hash = sysFile.FileHash;         // 文件哈希值（用于校验）
            int quality = uploadDto.Quality;   // 图片压缩质量（0表示不压缩）
            long userId = sysFile.CreateId;     // 创建用户ID

            // 构建阿里云OSS对象名称（完整路径）
            string objectName = Path.Combine(storeDir, $"{fileName}{ext}").Replace("\\", "/");
            HttpStatusCode statusCode;

            // 判断是否需要图片压缩处理
            if (quality > 0 && IsImageExt(ext))
            {
                // 图片压缩：先将压缩后的图片写入内存流，再上传
                using var stream = new MemoryStream();
                await CompressImageAsync(file, stream, quality, ext);
                stream.Position = 0; // 重置流指针到起始位置
                statusCode = AliyunOssHelper.PutObjectFromFile(stream, objectName, file.ContentType, _storageSettings.AliOss);
            }
            else
            {
                // 非图片或无需压缩：直接读取文件流上传
                using var stream = file.OpenReadStream();
                statusCode = AliyunOssHelper.PutObjectFromFile(stream, objectName, file.ContentType, _storageSettings.AliOss);
            }

            // 校验上传结果，失败则抛出异常
            if (statusCode != HttpStatusCode.OK)
                throw new Exception($"阿里云OSS上传失败，状态码：{statusCode}");

            // 构建文件访问URL
            string accessUrl = $"https://{_aliyunDomainUrl}/{objectName}";

            // 插入数据库并返回完整的文件信息实体
            return await InsertFileBaseAsync(file, userId, fileName, ext, sizeBytes, hash,
                (int)StoreType.ALIYUN, uploadDto.StoragePath ?? 0, objectName, accessUrl, uploadDto.Status ?? 1, uploadDto.Visibility);
        }

        // 辅助方法：判断文件扩展名是否为图片类型
        private bool IsImageExt(string ext)
        {
            var imageExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" };
            return imageExtensions.Contains(ext.ToLower());
        }

        /// <summary>
        /// 插入文件基础信息
        /// </summary>
        private async Task<FileBase> InsertFileBaseAsync(IFormFile file, long userId, string fileName, string ext, long sizeBytes, string hash, int storageType, int storagePath, string fileUrl, string accessUrl, int status, int? visibility)
        {
            var fileBase = new FileBase
            {
                RealName = file.FileName,
                Name = $"{fileName}{ext}",
                FileUrl = fileUrl,
                StorageType = storageType,
                StoragePath = storagePath,
                AccessUrl = accessUrl,
                FileHash = hash,
                FileSize = sizeBytes,
                FileType = file.ContentType,
                FileExt = ext,
                Visibility = visibility ?? 1,
                Status = status,
                CreateId = userId,
                CreateTime = DateTime.Now
            };

            fileBase.Id = await Context.Insertable(fileBase).ExecuteReturnSnowflakeIdAsync();
            return fileBase;
        }

        /// <summary>
        /// 保存文件配置
        /// </summary>
        private async Task SaveFileConfigAsync(long fileId, FileDtoBase dto)
        {
            if (!dto.Visibility.HasValue)
                return;

            switch (dto.Visibility.Value)
            {
                case (int)FileVisibility.Free:
                    if (dto.FileFree == null)
                        throw new ArgumentNullException(nameof(dto.FileFree), "免费文件配置不能为空");
                    var fileFree = dto.FileFree.Adapt<FileFree>();
                    fileFree.FileId = fileId;
                    await Context.Insertable(fileFree).ExecuteCommandAsync();
                    break;

                case (int)FileVisibility.PayPerDownload:
                    if (dto.FilePayPerDownload == null)
                        throw new ArgumentNullException(nameof(dto.FilePayPerDownload), "按次收费配置不能为空");
                    var filePay = dto.FilePayPerDownload.Adapt<FilePayPerDownload>();
                    filePay.FileId = fileId;
                    await Context.Insertable(filePay).ExecuteCommandAsync();
                    break;

                case (int)FileVisibility.MemberOnly:
                    if (dto.FileMember == null)
                        throw new ArgumentNullException(nameof(dto.FileMember), "会员专属配置不能为空");
                    var fileMember = dto.FileMember.Adapt<FileMember>();
                    fileMember.FileId = fileId;
                    await Context.Insertable(fileMember).ExecuteCommandAsync();
                    break;

                default:
                    throw new NotSupportedException($"不支持的文件可见性类型：{dto.Visibility.Value}");
            }
        }

        /// <summary>
        /// 图片压缩保存
        /// </summary>
        private async Task SaveCompressedImageAsync(IFormFile file, string savePath, int quality)
        {
            using var image = await Image.LoadAsync(file.OpenReadStream());
            var encoder = GetImageEncoder(Path.GetExtension(savePath), quality);
            await using var stream = new FileStream(savePath, FileMode.Create);
            await image.SaveAsync(stream, encoder);
        }

        /// <summary>
        /// 图片压缩到流
        /// </summary>
        private async Task CompressImageAsync(IFormFile file, MemoryStream stream, int quality, string fileExt)
        {
            using var image = await Image.LoadAsync(file.OpenReadStream());
            var encoder = GetImageEncoder(fileExt, quality);
            await image.SaveAsync(stream, encoder);
        }

        /// <summary>
        /// 获取图片编码器
        /// </summary>
        private IImageEncoder GetImageEncoder(string ext, int quality)
        {
            string lowerExt = ext.ToLowerInvariant();

            switch (lowerExt)
            {
                case ".png":
                    return new PngEncoder
                    {
                        CompressionLevel = PngCompressionLevel.DefaultCompression
                    };
                case ".jpg":
                case ".jpeg":
                    return new JpegEncoder
                    {
                        Quality = quality
                    };
                case ".gif":
                    return new GifEncoder();
                case ".bmp":
                    return new BmpEncoder();
                default:
                    throw new NotSupportedException($"不支持的图片格式压缩：{ext}");
            }
        }


        /// <summary>
        /// 生成访问地址（适配storageType=1路径规则）
        /// </summary>
        private string GenerateAccessUrl(int storageType, string storagePath, string fileName, int storagePathId, int visibility, string storeDir = "")
        {
            string normalizedStoragePath = storagePath?.Replace("\\", "/") ?? "";
            string normalizedFileName = fileName?.Replace("\\", "/") ?? "";

            switch (storageType)
            {
                case (int)StoreType.LOCAL:
                    if (string.IsNullOrEmpty(normalizedStoragePath) || string.IsNullOrEmpty(normalizedFileName))
                        throw new ArgumentException("文件路径或文件名不能为空");

                    // 计算相对路径（根路径 + 存储目录）
                    string rootPath = GetLocalStoragePathWithVisibility(storagePathId, visibility);
                    string fullLocalPath = string.IsNullOrEmpty(storeDir)
                        ? Path.Combine(rootPath, normalizedFileName)
                        : Path.Combine(rootPath, storeDir, normalizedFileName);

                    string relativePath = Path.GetRelativePath(
                        rootPath.Replace("\\", "/"),
                        fullLocalPath.Replace("\\", "/")
                    ).Replace("\\", "/");

                    string baseUrl = _uploadSettings.UploadUrl ?? "";
                    if (!baseUrl.EndsWith("/") && !string.IsNullOrEmpty(baseUrl))
                        baseUrl += "/";

                    // 拼接最终访问URL
                    if (string.IsNullOrEmpty(baseUrl))
                    {
                        return relativePath ?? string.Empty;
                    }

                    // 如果 relativePath 是绝对路径，直接返回它
                    if (!string.IsNullOrEmpty(relativePath) && Uri.IsWellFormedUriString(relativePath, UriKind.Absolute))
                    {
                        return relativePath;
                    }

                    // 正常拼接
                    return new Uri(new Uri(baseUrl), relativePath ?? string.Empty).ToString();

                case (int)StoreType.ALIYUN:
                    string aliyunPath = Path.Combine(normalizedStoragePath, normalizedFileName).Replace("\\", "/");
                    return $"https://{_aliyunDomainUrl}/{aliyunPath}";
                default:
                    return string.Empty;
            }
        }

        /// <summary>
        /// 获取用户类型
        /// </summary>
        private async Task<string> GetUserTypeAsync(long userId)
        {
            var user = await Context.Queryable<UserBase>()
                .Where(u => u.Id == userId)
                .Select(u => new { u.UserType })
                .FirstAsync();

            return user?.UserType ?? ((int)UserType.Personal).ToString();
        }

        /// <summary>
        /// 移动物理文件
        /// </summary>
        private async Task<bool> MovePhysicalFileAsync(dynamic oldFile, int newStorageType, int newStoragePath)
        {
            if (oldFile.StorageType == (int)StoreType.LOCAL && newStorageType == (int)StoreType.LOCAL)
            {
                // 本地存储内迁移：按新的visibility规则生成目标路径
                string oldFilePath = oldFile.FileUrl;
                string newRootPath = GetLocalStoragePathWithVisibility(newStoragePath, oldFile.Visibility);
                string newFilePath = Path.Combine(newRootPath, oldFile.Name);

                if (LocalFileHelper.Exists(oldFilePath) && !LocalFileHelper.Exists(newFilePath))
                {
                    LocalFileHelper.Move(oldFilePath, newFilePath);
                    return true;
                }
            }
            // 跨存储类型迁移（本地→阿里云等）可在此扩展
            await Task.CompletedTask;
            return true;
        }

        /// <summary>
        /// 删除物理文件
        /// </summary>
        private async Task<bool> DeletePhysicalFileAsync(dynamic fileInfo)
        {
            if (fileInfo.StorageType == (int)StoreType.LOCAL)
            {
                string filePath = fileInfo.FileUrl;
                if (LocalFileHelper.Exists(filePath))
                {
                    LocalFileHelper.Delete(filePath);
                    return true;
                }
            }
            // 云存储文件删除可在此扩展
            await Task.CompletedTask;
            return true;
        }

        /// <summary>
        /// 获取文件流
        /// </summary>
        private async Task<Stream> GetFileStreamAsync(FileVo fileVo)
        {
            if (fileVo.StorageType == (int)StoreType.LOCAL && LocalFileHelper.Exists(fileVo.FileUrl))
            {
                return new FileStream(fileVo.FileUrl, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
            }
            // 云存储文件流获取可在此扩展
            await Task.CompletedTask;
            return new MemoryStream();
        }

        /// <summary>
        /// 获取分片文件流
        /// </summary>
        private async Task<Stream> GetRangeFileStreamAsync(FileVo fileVo, long start, long end)
        {
            if (fileVo.StorageType == (int)StoreType.LOCAL && LocalFileHelper.Exists(fileVo.FileUrl))
            {
                var stream = new FileStream(fileVo.FileUrl, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
                stream.Seek(start, SeekOrigin.Begin);
                var buffer = new byte[end - start + 1];
                await stream.ReadAsync(buffer, 0, buffer.Length);
                return new MemoryStream(buffer);
            }
            // 云存储分片流获取可在此扩展
            await Task.CompletedTask;
            return new MemoryStream();
        }

        /// <summary>
        /// 解析范围请求头
        /// </summary>
        private (long start, long end) ParseRangeHeader(string rangeHeader, long fileSize)
        {
            if (string.IsNullOrEmpty(rangeHeader) || !rangeHeader.StartsWith("bytes="))
                return (0, fileSize - 1);

            var range = rangeHeader["bytes=".Length..].Split('-');
            if (!long.TryParse(range[0], out long start))
                start = 0;
            if (range.Length < 2 || !long.TryParse(range[1], out long end))
                end = fileSize - 1;

            start = Math.Max(0, Math.Min(start, fileSize - 1));
            end = Math.Max(start, Math.Min(end, fileSize - 1));
            return (start, end);
        }

        /// <summary>
        /// 更新下载统计
        /// </summary>
        private async Task UpdateDownloadStatsAsync(long fileId, int visibility, long userId)
        {
            // 可扩展：更新fileFree/filePayPerDownload/fileMember表的下载统计字段
            await Task.CompletedTask;
        }

        /// <summary>
        /// 记录下载日志
        /// </summary>
        private async Task RecordDownloadLogAsync(long fileId, long userId)
        {
            await Context.Insertable(new FileDownloadRecord
            {
                FileId = fileId,
                UserId = userId,
                DownloadTime = DateTime.Now
            }).ExecuteCommandAsync();
        }

        /// <summary>
        /// 获取分片存储路径（适配storageType=1路径规则）
        /// </summary>
        private string GetChunkStoragePath(string uploadId, int chunkIndex, int storagePathId, int visibility)
        {
            return Path.Combine(
                GetLocalStoragePathWithVisibility(storagePathId, visibility),
                "chunks",
                uploadId,
                $"{chunkIndex}{Path.GetExtension(uploadId) ?? ".chunk"}"
            );
        }

        /// <summary>
        /// 保存分片到存储
        /// </summary>
        private async Task SaveChunkToStorageAsync(IFormFile file, string chunkPath, StoreType storageType)
        {
            if (storageType == StoreType.LOCAL)
            {
                string chunkDir = Path.GetDirectoryName(chunkPath);
                if (!Directory.Exists(chunkDir))
                    Directory.CreateDirectory(chunkDir);

                using var stream = new FileStream(chunkPath, FileMode.Create);
                await file.CopyToAsync(stream);
            }
            // 云存储分片保存可在此扩展
            await Task.CompletedTask;
        }

        /// <summary>
        /// 合并分片并保存（适配storageType=1路径规则）
        /// </summary>
        private async Task<FileBase> MergeChunksAndSaveAsync(MultipartUploadMeta uploadMeta, string fileName, string storeDir, long userId, int visibility, int storagePathId)
        {
            var chunks = await Context.Queryable<MultipartUploadChunk>()
                .Where(c => c.UploadId == uploadMeta.UploadId)
                .OrderBy(c => c.ChunkIndex)
                .ToListAsync();

            if (chunks.Count == 0)
                throw new InvalidOperationException("无分片文件可合并");

            // 合并分片到目标文件
            using var targetStream = new FileStream(storeDir, FileMode.Create);
            foreach (var chunk in chunks)
            {
                using var chunkStream = new FileStream(chunk.ChunkPath, FileMode.Open);
                await chunkStream.CopyToAsync(targetStream);
            }

            // 生成文件信息
            string fileExt = uploadMeta.FileExt;
            string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
            string fullFileName = $"{fileNameWithoutExt}{fileExt}";
            string fileHash = uploadMeta.FileHash;
            long fileSize = uploadMeta.FileSize;

            // 生成访问地址
            string accessUrl = GenerateAccessUrl(
                (int)uploadMeta.StorageType,
                storeDir,
                fullFileName,
                storagePathId,
                visibility
            );

            return await InsertFileBaseAsync(
                new FormFile(targetStream, 0, fileSize, fileName, fullFileName),
                userId,
                fileNameWithoutExt,
                fileExt,
                fileSize,
                fileHash,
                uploadMeta.StorageType,
                storagePathId,
                storeDir.Replace("\\", "/"),
                accessUrl,
                1, // 默认为正常状态
                visibility
            );
        }

        /// <summary>
        /// 清理分片临时数据
        /// </summary>
        private async Task CleanupMultipartTempDataAsync(string uploadId)
        {
            // 删除分片记录
            await Context.Deleteable<MultipartUploadChunk>().Where(c => c.UploadId == uploadId).ExecuteCommandAsync();
            // 删除元数据
            await Context.Deleteable<MultipartUploadMeta>().Where(m => m.UploadId == uploadId).ExecuteCommandAsync();

            // 清理物理分片文件
            var chunks = await Context.Queryable<MultipartUploadChunk>()
                .Where(c => c.UploadId == uploadId)
                .Select(c => c.ChunkPath)
                .ToListAsync();
            foreach (var chunkPath in chunks)
            {
                if (LocalFileHelper.Exists(chunkPath))
                    LocalFileHelper.Delete(chunkPath);
            }
        }

        /// <summary>
        /// 校验免费文件访问权限
        /// </summary>
        private async Task<FileAccessResult> CheckFreeFileAccessAsync(FileVo fileVo, long userId)
        {
            // 可扩展：免费文件下载次数限制校验
            var freeConfig = await Context.Queryable<FileFree>()
                .Where(ff => ff.FileId == fileVo.Id)
                .FirstAsync();

            if (freeConfig?.DownloadLimit > 0)
            {
                int downloadCount = await Context.Queryable<FileDownloadRecord>()
                    .Where(r => r.FileId == fileVo.Id && r.UserId == userId)
                    .CountAsync();

                if (downloadCount >= freeConfig.DownloadLimit)
                    return new FileAccessResult(false, $"免费下载次数已用尽（限{freeConfig.DownloadLimit}次）");
            }

            await Task.CompletedTask;
            return new FileAccessResult(true, "免费文件可访问");
        }

        /// <summary>
        /// 校验按次付费文件访问权限
        /// </summary>
        private async Task<FileAccessResult> CheckPayFileAccessAsync(long fileId, long userId)
        {
            // 可扩展：付费记录校验（是否已购买该文件）
            await Task.CompletedTask;
            return new FileAccessResult(true, "已付费，可访问");
        }

        /// <summary>
        /// 校验会员专属文件访问权限
        /// </summary>
        private async Task<FileAccessResult> CheckMemberFileAccessAsync(FileVo fileVo, long userId)
        {
            // 可扩展：会员等级校验
            var memberConfig = await Context.Queryable<FileMember>()
                .Where(fm => fm.FileId == fileVo.Id)
                .FirstAsync();

            if (memberConfig == null)
                return new FileAccessResult(false, "会员文件配置不存在");

            // 假设会员等级存储在UserBase表的MemberLevel字段
            var user = await Context.Queryable<UserBase>()
                .Where(u => u.Id == userId)
                .Select(u => new { u.UserIndividualNav })
                .FirstAsync();

            if (user == null || user.UserIndividualNav.MembershipLevel < memberConfig.RequiredMemberLevel)
                return new FileAccessResult(false, $"会员等级不足（需{memberConfig.RequiredMemberLevel}级及以上）");

            await Task.CompletedTask;
            return new FileAccessResult(true, "会员用户可访问");
        }

        public Task<long> InsertFileAsync(FileDto dto, long currentUserId, StoreType storeType = StoreType.LOCAL)
        {
            // 实现重载方法（如果需要）
            throw new NotImplementedException();
        }

        

        #endregion
    }
}